/*
 * Simulator
 * Date: December 09, 2011
 * Group JEDi
 * Author(s): James Lovato, Efim Todorasco, Daniel Garrison
 */
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

public class Simulator implements KeyListener{
	private static final String NEW_LINE = "\n";
	private static final String BORDER_STAR = "\n************************************************************\n";
	private static final String SYS_CALL_MESSAGE = BORDER_STAR + NEW_LINE + "\t\t  SYSTEM CALL" + 
	NEW_LINE + BORDER_STAR;
	private static final String TIMER_INT_MESSAGE = BORDER_STAR + NEW_LINE + "\t\t  TIMER INTERRUPT" + 
	NEW_LINE + BORDER_STAR;
	private static final String KERNEL_ENTER_MESSAGE = BORDER_STAR + NEW_LINE + "\t\t  ENTERING KERNEL SPACE";
	private static final String KERNEL_EXIT_MESSAGE = "\t\t  LEAVING KERNEL SPACE" + 
	NEW_LINE + BORDER_STAR;
	private static final String SEM_DOWN_TRAP_MESSAGE = "semaphore_down_trap_handler";
	private static final String SEM_UP_TRAP_MESSAGE = "semaphore_up_trap_handler";
	private static final String MEM_WRITE_TRAP_MESSAGE = "mem_write_trap_handler";
	private static final String MEM_READ_TRAP_MESSAGE = "mem_read_trap_handler";

	private static final int SIM_SPEED = 1000;
	private static final int LOCK_TIME = 10;  // DO NOT CHANGE THIS VALUE!!!
	private static final int DISK_DELAY = 10000;
	private static final int VIDEO_DELAY = 5000;
	private static final int KEY_DELAY = 2000;
	public static final int MIN_REQUESTS = 1;
	public static final int MAX_REQUESTS = 5;
	public static final int MIN_INSTRUCTIONS = 1000;
	public static final int MAX_INSTRUCTIONS = 10000;
	String[] args;
	public BlockingQueue<Interrupts> interruptQueue =  new LinkedBlockingQueue<Interrupts>(); 
	boolean mutex = false;
	boolean semWrite = true;
	boolean sharedValue = false;
	boolean semRead = false;
	Queue<Process> diskQueue = new ArrayDeque<Process>();
	Queue<Process> videoQueue = new ArrayDeque<Process>();
	Queue<Process> readyQueue = new ArrayDeque<Process>();
	static Queue<Character> keyQueue = new ArrayDeque<Character>();
	private ExecutorService myPool = Executors.newFixedThreadPool(10);
	Process currentProcess;
	Process idleProcess;
	CPU myCpu = new CPU();
	Timer myTimer = new Timer();
	Disk myDisk = new Disk();
	Video myVideo = new Video();
	KeyBoard myKeyBoard = new KeyBoard();
	Interrupts inter = Interrupts.CALCULATOR;
	StringBuilder cpuSB = new StringBuilder();
	StringBuilder timerSB = new StringBuilder();
	StringBuilder diskSB = new StringBuilder();
	StringBuilder videoSB = new StringBuilder();
	StringBuilder keyBoardSB = new StringBuilder();
	String runningDots = "";
	
	private final JFrame frame = new JFrame("Sheduler");
	private JPanel cons = new JPanel();
	public static JTextArea text = new JTextArea();
    File file = new File("output.txt");

	
	public Simulator(String[] arg) {
		args = arg;
		idleProcess = new Process();
		idleProcess.numOfInstr = 1000000;
		createProcesses();
		currentProcess = readyQueue.poll();
		currentProcess.setState(State.RUNNING);
		myPool.execute(myTimer);
		myPool.execute(myDisk);
		myPool.execute(myVideo);
		myPool.execute(myKeyBoard);
		myPool.execute(myCpu);
		start();
	}
	
	public void start(){
		text.setEditable(false);
		text.addKeyListener(this);
		Font font = new Font("Courier", Font.PLAIN, 14);
        text.setFont(font);
        text.setBackground(Color.BLACK);
        text.setForeground(Color.GREEN);
		JScrollPane scroll_text_area = new JScrollPane(text);
		scroll_text_area.setVerticalScrollBarPolicy(
		                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
		scroll_text_area.setPreferredSize(new Dimension(600, 500));
		scroll_text_area.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener(){
			public void adjustmentValueChanged(AdjustmentEvent e){
			text.select(text.getHeight()+1000000,0);
			}});
		cons.setLayout(new BorderLayout()); 
	    cons.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
		cons.add(scroll_text_area, BorderLayout.CENTER);
	}
	
	public void createProcesses(){
		// io calc proNcon proNconWextraIO
		// i  c    p       w
		//String arr[] = {"i", "c", "p", "w"};
		//args = arr;
		Random rand = new Random();
		for (int i = 0; i < args.length; i++) {
			if (args[i].equals("i")) {
				Process p = new Process();
				p.typeOfProcess = ProcessType.IO;
				readyQueue.add(p);
			} else if (args[i].equals("c")) {
				Process p = new Process();
				p.typeOfProcess = ProcessType.CALCULATOR;
				readyQueue.add(p);
			} else if (args[i].equals("p")) {
				Process p = new Process();
				p.typeOfProcess = ProcessType.PRODUCER;
				readyQueue.add(p);
				Process c = new Process();
				p.typeOfProcess = ProcessType.CONSUMER;
				p.addProducer();
				c.addConsumer();
				readyQueue.add(p);
				readyQueue.add(c);
			} else if (args[i].equals("w")) {
				Process p = new Process(rand.nextInt(LOCK_TIME));
				p.typeOfProcess = ProcessType.PRODUCER;
				readyQueue.add(p);
				Process c = new Process(rand.nextInt(LOCK_TIME));
				p.typeOfProcess = ProcessType.CONSUMER;
				p.addProducer();
				c.addConsumer();
				readyQueue.add(p);
				readyQueue.add(c);
			}
		}

	}
	
	public void kernel(Interrupts interupt) {
		cpuSB.append(NEW_LINE);
		cpuSB.append(KERNEL_ENTER_MESSAGE);
		cpuSB.append(NEW_LINE);
		switch (interupt) {
		case DISK:
			cpuSB.append("disk_io_trap_handler");
			currentProcess.setState(State.BLOCKED); // block the running process
			cpuSB.append(NEW_LINE);
			cpuSB.append(currentProcess.getProcessString() + 
					" state = " + currentProcess.pcBlock.state + " on Device: Disk");
			diskQueue.add(currentProcess); // process blocked on disk device
			cpuSB.append(NEW_LINE);
			cpuSB.append("Device: Disk blocked queue size = " + diskQueue.size());
			scheduler();
			break;
		case VIDEO:
			cpuSB.append("video_io_trap_handler");
			currentProcess.setState(State.BLOCKED);
			cpuSB.append(NEW_LINE);
			cpuSB.append(currentProcess.getProcessString() + 
					" state = " + currentProcess.pcBlock.state + " on Device: Video");
			videoQueue.add(currentProcess);
			cpuSB.append(NEW_LINE);
			cpuSB.append("Device: Video blocked queue size = " + videoQueue.size());
			scheduler();
			break;
		case DISK_READY:
			cpuSB.append("Disk I/O complete for ");
			Process disk_return = diskQueue.poll();
			cpuSB.append(disk_return.getProcessString());
			disk_return.setState(State.READY);
			cpuSB.append(NEW_LINE);
			cpuSB.append(disk_return.getProcessString() + " State = " + disk_return.getState());
			readyQueue.add(disk_return);
			if (currentProcess == idleProcess) {
				scheduler();
			} else {
				currentProcess.setState(State.RUNNING);
				cpuSB.append(NEW_LINE);
				cpuSB.append(noContextSwitch());
			}
			break;
		case VIDEO_READY:
			cpuSB.append("Video I/O complete for ");
			Process video_return = videoQueue.poll();
			cpuSB.append(video_return.getProcessString());
			video_return.setState(State.READY);
			cpuSB.append(NEW_LINE);
			cpuSB.append(video_return.getProcessString() + 
					" State = " + video_return.getState());
			readyQueue.add(video_return);
			if (currentProcess == idleProcess) {
				scheduler();
			} else {
				currentProcess.setState(State.RUNNING);
				cpuSB.append(NEW_LINE);
				cpuSB.append(noContextSwitch());			
			}
			break;
		case KEYBOARD:
			char key_return = keyQueue.poll();
			cpuSB.append("key_board_trap_handler");
			cpuSB.append(NEW_LINE);
			cpuSB.append("Key '" + key_return + "' was pressed");
			currentProcess.setState(State.RUNNING);
			cpuSB.append(NEW_LINE);
			cpuSB.append(noContextSwitch());
			break;
		case LOCK_MUTEX:
			if (sharedValue) {
				cpuSB.append(MEM_READ_TRAP_MESSAGE);
			} else {
				cpuSB.append(MEM_WRITE_TRAP_MESSAGE);
			}
			if (mutex) {
				cpuSB.append(NEW_LINE);
				cpuSB.append(currentProcess.getProcessString()+ " Cannot Lock Mutex.");
				cpuSB.append(NEW_LINE);
				addSharedValueInfo();
				scheduler();
			} else {
				mutex = true;
				currentProcess.setState(State.RUNNING); // own mutex and proceed.
				cpuSB.append(NEW_LINE);
				addSharedValueInfo();
				cpuSB.append(NEW_LINE);
				cpuSB.append(noContextSwitch());
			}
			break;
		case UNLOCK_MUTEX:
			cpuSB.append(currentProcess.getProcessString() + " Unlocks Mutex.");
			mutex = false; 
			currentProcess.setState(State.RUNNING);
			cpuSB.append(NEW_LINE);
			addSharedValueInfo();
			cpuSB.append(NEW_LINE);
			cpuSB.append(noContextSwitch());
			break;
		case SEM_READ_UP:
			semRead = true;
			currentProcess.setState(State.RUNNING);
			cpuSB.append(currentProcess.getProcessString() + " UP Read Semaphore");
			cpuSB.append(NEW_LINE);
			addSharedValueInfo();
			cpuSB.append(NEW_LINE);
			cpuSB.append(noContextSwitch());
			break;
		case SEM_READ_DOWN:
			if (semRead) {
				semRead = false; // own the sem and proceed
				currentProcess.setState(State.RUNNING);
				cpuSB.append(currentProcess.getProcessString() + " Owns Read Semaphore");
				cpuSB.append(NEW_LINE);
				addSharedValueInfo();
				cpuSB.append(NEW_LINE);
				cpuSB.append(noContextSwitch());
			} else {
				cpuSB.append(currentProcess.getProcessString() + " Cannot Owns Read Semaphore");
				cpuSB.append(NEW_LINE);
				addSharedValueInfo();
				scheduler(); // block on read down
			}
			break;
		case SEM_WRITE_UP:
			semWrite = true;
			currentProcess.setState(State.RUNNING);
			cpuSB.append(currentProcess.getProcessString() + " UP Read Semaphore");
			cpuSB.append(NEW_LINE);
			addSharedValueInfo();
			cpuSB.append(NEW_LINE);
			cpuSB.append(noContextSwitch());
			break;
		case SEM_WRITE_DOWN:
			cpuSB.append(SEM_DOWN_TRAP_MESSAGE);
			if (semWrite) {
				semWrite = false; // own the sem and proceed
				currentProcess.setState(State.RUNNING);
				cpuSB.append(currentProcess.getProcessString() + " Owns Write Semaphore");
				cpuSB.append(NEW_LINE);
				addSharedValueInfo();
				cpuSB.append(NEW_LINE);
				cpuSB.append(noContextSwitch());
			} else {
				cpuSB.append(currentProcess.getProcessString() + " Cannot Owns Write Semaphore");
				addSharedValueInfo();
				cpuSB.append(NEW_LINE);
				scheduler(); // block on write down
			}
			break;
		case TIMER:
			cpuSB.append("timer_interrupt_handler");
			scheduler();
			break;
		default:
			break;
		}
		currentProcess.setState(State.RUNNING);
		cpuSB.append(NEW_LINE);
		cpuSB.append(KERNEL_EXIT_MESSAGE);
	}
	
	private void addSharedValueInfo() {
		cpuSB.append(NEW_LINE);
		cpuSB.append("Write Semaphore = " + (semWrite ? "UP" : "DOWN"));
		cpuSB.append(NEW_LINE);
		cpuSB.append("" + (mutex ? "Producer" : "Consumer") + " owns Mutex");
		cpuSB.append(NEW_LINE);
		cpuSB.append("Shared Value = " + sharedValue);
		cpuSB.append(NEW_LINE);
		cpuSB.append("Read Semaphore = " + (semRead ? "UP" : "DOWN"));
	}
	
	private String noContextSwitch() {
		return "No Context Switch: Running " + currentProcess.getProcessString();
	}
	
	private String contextSwitch() {
		return "Context Switch: Running " + currentProcess.getProcessString();
	}

	/********************************************
	 * Inner classes for threads.  Must be inner
	 ********************************************/
	
	private class CPU implements Runnable {
		@Override
		public void run() {
			
			while(true){ 
				cpuSB = new StringBuilder();
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
						e.printStackTrace();
				}
				
				if (!interruptQueue.isEmpty()) {
					Interrupts interrupts = interruptQueue.poll();
					currentProcess.setState(State.INTERRUPTED);
					if (interrupts == Interrupts.CRIT_REGION) {
						sharedValue = !sharedValue;
						currentProcess.setState(State.RUNNING);
						cpuSB.append(BORDER_STAR + NEW_LINE + "\t\t " + currentProcess.getProcessString() +
								" set Shared Data value to " + sharedValue);
					} else {
						if (interrupts == Interrupts.TIMER) {
							// print the timer interrupt message;
							cpuSB.append(NEW_LINE);
							cpuSB.append(TIMER_INT_MESSAGE);
						} else {
							// print the sys call interrupt message
							cpuSB.append(NEW_LINE);
							cpuSB.append(SYS_CALL_MESSAGE);
						}
						cpuSB.append(NEW_LINE);
						cpuSB.append(currentProcess.getProcessString());
						cpuSB.append(" state =  ");
						cpuSB.append(currentProcess.getState());
						cpuSB.append(" at instruction ");

					// saving pcb
					currentProcess.pcBlock.nextStep = currentProcess.currInst + 1;
					cpuSB.append(currentProcess.currInst);
					kernel(interrupts);
					text.append(cpuSB.toString());
					textWriter(cpuSB.toString());
						
					}
					
					//System.out.println(cpuSB);
				} else if (currentProcess == idleProcess && !readyQueue.isEmpty()){ // 
					//currentProcess = readyQueue.poll();
					scheduler();
					// readyQueue.add(idleProcess);
				} else if(!currentProcess.getState().equals(State.RUNNING)) {
					System.err.println("ERROR: current running process is not State.Running but " +
							currentProcess.getState().toString());
				} else { // increment  current process
					if (currentProcess.getState() == State.RUNNING) {
						Interrupts interrupt = currentProcess.incProcess();
						text.append(".");
						//System.out.print(".");
						if (interrupt != null) {
							try {
								interruptQueue.put(interrupt);
							} catch (InterruptedException e) {
								//e.printStackTrace();
							}
						}
					}
				} // end increment current process
			}  // end while
		}
	}
	
	private class Timer implements Runnable {
	
		@Override
		public void run() {
			while (true) {
				try {
					Thread.sleep(5125);
					interruptQueue.put(Interrupts.TIMER);				
				} catch (InterruptedException e) {
						e.printStackTrace();
				}
			}
		}
	}
	
	public class Disk implements Runnable {
		@Override
		public void run() {
			while(true) {
				try {
					Thread.sleep(DISK_DELAY);
					if (!diskQueue.isEmpty()) {
						interruptQueue.put(Interrupts.DISK_READY);
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public class Video implements Runnable {
		@Override
		public void run() {
			while(true) {
				try {
					Thread.sleep(VIDEO_DELAY);
					if (!videoQueue.isEmpty()) {
						interruptQueue.put(Interrupts.VIDEO_READY);
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	public class KeyBoard implements Runnable {
		@Override
		public void run() {
			while(true) {

				if (!keyQueue.isEmpty()) {
				  try {
					//Thread.sleep(KEY_DELAY);
					interruptQueue.put(Interrupts.KEYBOARD);

					Thread.sleep(KEY_DELAY);

				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				} else {
					System.out.print("");
				}
			}
		}
	}
	
	public void scheduler() {
		// get the next process
		// if the process is idle check for another ready process
		//		put idle back on queue
		//		get the next process
		// if the process is null there are no other processes so run what you have.
		//System.err.println("readyQueue.size() scheduler start = " + readyQueue.size());
		
		if (currentProcess.getState() == State.INTERRUPTED) {
			if (currentProcess != idleProcess) {
				// do not set idleProcess state
				// do not put idleProcess in readyQueue
				// if the process is not the idle process and its state is INTERRUPTED 
				//    (i.e. it is NOT BLOCKED) then set its state to ready and put it 
				//    into the readyQueue.
				currentProcess.setState(State.READY);				
				readyQueue.add(currentProcess);
				cpuSB.append(NEW_LINE);
				cpuSB.append(currentProcess.getProcessString()+ " state = " + 
						currentProcess.getState().toString());
			}
		}
		if (readyQueue.isEmpty()) { 
			// if the ready queue is empty and the current process has been blocked (implicitly from above
			// run the idleProcess.
			currentProcess = idleProcess;
			currentProcess.setState(State.RUNNING);
			cpuSB.append(NEW_LINE);
			cpuSB.append("No available process CPU is running IDLE");
		} else {
			//otherwise there are process in the ready queue so run one of those.
			currentProcess = readyQueue.poll();
			cpuSB.append(NEW_LINE);
			cpuSB.append(contextSwitch());
		}

	

	}
	public void guiFrame(){
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		cons.setOpaque(true);
		frame.setContentPane(cons);
		frame.pack();
		frame.setVisible(true);
	}
	@Override
	public void keyPressed(KeyEvent e) {
		char s = e.getKeyChar();
		if (s == ' ') {
			try {
				interruptQueue.put(Interrupts.TIMER);
			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}				
		} else {
		keyQueue.add(s);
		}
	}
	@Override
	public void keyReleased(KeyEvent arg0) {
		// TODO Auto-generated method stub
		
	}
	@Override
	public void keyTyped(KeyEvent arg0) {
		// TODO Auto-generated method stub
		
	}
	private void textWriter(String the_output) {
		Writer output = null;
		try {
			output = new BufferedWriter(new FileWriter(file, true));
			output.append(the_output);
		    output.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		System.out.println("Your file has been written");
	}
}
